package com.nulldev.util.web.nTubeExtensions.sponsorFree;

import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.JVM.arguments.ArgumentWithValue;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.fuckTracking.FuckTracking;
import com.nulldev.util.web.nTube.util.NTubeUtils;
import com.nulldev.util.web.nTubeExtensions.sponsorFree.SponsorBlockAPI.SponsorMeta.SponsorSegment;

public class SponsorBlockAPI {

	// TODO: Add support for using a local database or a URL database...

	private static final int API_VERSION = Arguments.hasArgument("--SponsorBlockAPI.useLegacyAPI") ? 1 : 2;
	private static final Logger log = LoggerFactory.getLogger();

	private static final String DEFAULT_API_FRONTEND = "https://sponsor.ajay.app";
	public static final ArgumentWithValue<String> API_FRONTEND = new ArgumentWithValue.StringArgument(new String[]
		{ "--nTube.sponsorBlockURL", "-nTube.sponsorBlockURL" }, "Allows a custom SponsorBlock backend to be used, i.e. self-hosted.",
			"A URL to the SponsorBlock frontend (default: https://sponsor.ajay.app)", true);

	private static final String GET_INST = API_VERSION == 2 ? "/api/skipSegments" : "/api/getVideoSponsorTimes";
	private static final int n = 2048;
	private static final LinkedHashMap<String, SponsorMeta> sponsor_cache = new LinkedHashMap<String, SponsorMeta>(n) {
		private static final long serialVersionUID = 1191884758746431786L;

		@Override
		protected boolean removeEldestEntry(final Map.Entry<String, SponsorMeta> entry) {
			return size() > n;
		}
	};

	public static class SponsorMeta {
		public static class SponsorSegment {
			public final float start;
			public final float end;
			public final String uuid;

			public SponsorSegment(final float start, final float end, final String uuid) {
				this.start = start;
				this.end = end;
				this.uuid = uuid;
			}

			@Override
			public String toString() {
				return "SponsorSegment[start=" + this.start + ",end=" + this.end + ",uuid=" + this.uuid + "]";
			}
		}

		private final SponsorSegment[] segments;

		public SponsorMeta(final SponsorSegment... segments) {
			this.segments = segments;
		}

		public SponsorSegment[] segments() {
			return this.segments;
		}
	}

	public static SponsorMeta auto(final String videoID) throws Exception {
		return auto(videoID, false);
	}

	public static SponsorMeta auto(final String videoID, final boolean disableFT) throws Exception {
		if (sponsor_cache.containsKey(videoID)) {
			return sponsor_cache.get(videoID);
		} else {
			return get(videoID, disableFT);
		}
	}

	public static SponsorMeta get(final String videoID) throws Exception {
		return get(videoID, false);
	}

	private static String getFrontend() {
		if (Arguments.get().has(API_FRONTEND)) {
			return Arguments.get().getValue(API_FRONTEND);
		} else {
			return DEFAULT_API_FRONTEND;
		}
	}

	private static final Entry[] BLANK_HEADERS = new Entry[0];

	public static SponsorMeta get(final String videoID, final boolean disableFT) throws Exception {
		if (videoID == null || videoID.isEmpty())
			return null;
		final String url = getFrontend() + GET_INST + "?videoID=" + videoID;
		try {
			final HttpResponse<String> r = HttpClient.getString().url(url).headers(disableFT ? BLANK_HEADERS : FuckTracking.protect())
					.timeout(Duration.ofSeconds(30)).create();
			if (r.status() == 404) {
				// no sponsors found :)
				IOUtils.closeQuietly(r);
				sponsor_cache.put(videoID, null);
				return null;
			}
			final String _r = r.dataThenClose();
			if (_r.contains("Internal Server Error") || _r.contains("Bad Gateway") || r.status() >= 500) {
				log.error("Server reported an internal error, bailing out. [response: " + r.status() + "]");
				IOUtils.closeQuietly(r);
				return null;
			}
			final UniJsonValue obj = UniJsonValue.parse(_r);
			if (API_VERSION == 2) {
				final UniJsonIterableValue segs = obj.asArray();
				SponsorSegment[] sms = new SponsorSegment[segs.length()];
				for (int i = 0; i < segs.length(); i++) {
					final UniJsonValue seg = segs.get(i);
					final String category = seg.getString("category", "unknown");
					if (!category.equalsIgnoreCase("sponsor")) {
						log.warn("Unknown segment category: " + category);
						continue;
					}
					final UniJsonValue o = seg.get("segment");
					final float start = o.get(0).asFloat();
					final float end = o.get(1).asFloat();
					if (start == -1 || end == -1) {
						log.warn("get(String): Skipped a invalid entry!");
						continue;
					} else {
						sms[i] = new SponsorSegment(start, end, seg.getString("UUID"));
						continue;
					}
				}
				final SponsorMeta ss = new SponsorMeta(sms);
				sponsor_cache.put(videoID, ss);
				return ss;
			} else {
				final int sponsors = obj.get("sponsorTimes").length();
				SponsorSegment[] sms = new SponsorSegment[sponsors];
				for (int i = 0; i < sponsors; i++) {
					final String uuid = obj.get("UUIDs").get(i).string();
					final UniJsonValue o = obj.get("sponsorTimes").get(i).asArray();
					final float start = o.get(0).asFloat();
					final float end = o.get(1).asFloat();
					if (start == -1 || end == -1) {
						log.warn("get(String): Skipped a invalid entry!");
						continue;
					} else {
						sms[i] = new SponsorSegment(start, end, uuid);
						continue;
					}
				}
				final SponsorMeta ss = new SponsorMeta(sms);
				sponsor_cache.put(videoID, ss);
				return ss;
			}
		} catch (Exception e) {
			if (NTubeUtils.isSlientNetworkException(e)) {
				log.error("Failed to get sponsor data! [err=TimeoutOrNetwork,vID=" + videoID + "]");
			} else {
				log.error("Failed to get sponsor data!", e);
			}
			return null;
		}
	}

	public static void main(String[] args) throws Exception {
		ArrayUtils.printLln(auto("uY_jeaxVgIE").segments);
		JVM.println("==");
		ArrayUtils.printLln(auto("uY_jeaxVgIE").segments);
		JVM.println("==");
		ArrayUtils.printLln(auto("uY_jeaxVgIE").segments);
		JVM.println("==");
	}
}
